package jmpo.io;

import java.io.StringWriter;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Stream;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.io.RandomAccessFile;

/**
 * Java IO - это базовые возможности работы с вводм и выводом (Input / Output)
 * в языке Java. Пакет java.io основан на системе синхронного потокового ввода / вывода.
 * Идея потока состоит в том, что мы читаем некоторые данные из этого потока, а система 
 * добавляет нам данные в этот поток из некоторого источника. Таким образом мы абстрагированы 
 * от конкретного источника и его особенностей с помощью сущности потока.  
 * Пакет содержит большое количество классов со сложной иерархией.
 * Основными элементами являются 4 абстракных класса:
 *     InputStream / Reader | OutputStream / Writer
 *            ввод                    вывод
 * 
 * InputStream / OutputStream - это базовые классы для байтового ввода и вывода
 * Reader      / Writer       - это базовые классы для символьного (строкового) ввода и вывода
 *
 * Рассмотрим иерархию работы с символьными потоками на примере Writer:
 * BufferedWriter -> Writer
 * StringWriter   -> Writer
 * FileWriter -> OutputStreamWriter -> Writer
 *
 * Writer определяет базовый набор методов:
 *     write методы позволяют записывать массивы символов и строки
 *     append больше ориентированы на работу с абстракциями - позволяют записывать CharSequence
 * реализовывать эти методы должны непосредственно наследники Writer
 *
 * BufferedWriter - не является самостоятельной реализацией Writer. Это декоратор, который позволяет 
 * создать промежуточный программный буфер заданного размера. В конструкторе он принимает
 * ссылку на другой Writer.
 * 
 * StringWriter - реализует поток вывода с помощью внутреннего символьного буфера StringBuffer, который 
 * позже можно преобразовать в строку вызовом toString.
 * 
 * OutputStreamWriter - это обёртка над байтовым потоком OutputStream, которая преобразует символы в 
 * поток байтов. Следует помнить, что символы в строке String пишутся в кодировке UTF-16 (равномерная
 * двухбайтовая кодировка), символы преобразуются в заданную кодировку, которая передаётся в
 * конструкторе, по умолчанию используется системная.
 * 
 * FileWriter - это удобный класс для записи данных в файл. У него есть набор конструкторов, 
 * позволяющих создавать объект как от объектов типа File, так и от системных путей, заданных строками.
 *      
 * Существует множество дополнительных декораторов для Writer, таких как FilterWriter или PipedWriter.
 * 
 * Работа с потоками всегда может закончиться ошибкой, так как она связана с внешней системой.
 * О таких ошибках сигнализируют исключения. Базовым исключением является IOException. Все эти исключения 
 * являются checked и должны быть указаны в сигнатуре метода или обработаны.
 * 
 * Writer наследует интерфейсу Closable, который в свою очередь наследует AutoClosable, что позволяет нам
 * использовать потоки в try-with-resources для автоматического возвращения ресурса системе.
 */

final class FileWriterExample {
    public static void test(String filename, CharSequence text) throws IOException {
        try (var fileWriter = new FileWriter(filename, true)) {
            fileWriter.append(text);
            System.out.println("The text has been successfully appended!");
        }
    }
}

final class StringWriterExample {
    public static void test(String text1, String text2) throws IOException {
        try (var stringWriter = new StringWriter()) {
            stringWriter.append(text1).append(":").append(text2);
            System.out.println(stringWriter.toString());
        }
    }
}

/**
 * Важным элементом потокового вывода является PrintWriter.
 * PrintWriter позволяет записывать символы в поток OutputStream или Writer 
 * принимая на вход типы Java. Похожим образом мы обыкновенно выводили данные 
 * в консоль.
 */
final class PrintWriterExample {
    public static void test(String filename, long[] numbers) throws IOException {
        try (var pw = new PrintWriter(new FileWriter(filename, true))) {
            for (var l : numbers)
                pw.println(l);
            System.out.println("Numbers have been successfully appended!");
        }
    }
}

/**
 * Для чтения данных из потока Reader удобным вляется BufferedReader, который позволяет 
 * читать данные сразу в строку. Он является декаратором и им можно обернуть FileReader.
 */
final class BufferedReaderExample {
    public static void test(String filename) throws IOException {
        try (var lfr = new BufferedReader(new FileReader(filename))) {
            String currentLine = null;
            long lineCounter = 0;
            do {
                currentLine = lfr.readLine();
                if (currentLine != null) {
                    // обработаем строку
                    System.out.format("%d: %s\n",++lineCounter, currentLine);
                }
            } while (null != currentLine);
        }
    }
}

/**
 * BufferedReader также предоставляет удобный метод для преобразования строк файла в поток (Stream API)
 */
final class BufferedReaderStreamExample {
    public static void test(String filename) throws IOException {
        try (var br = new BufferedReader(new FileReader(filename))) {
            Stream<String> ss = br.lines();
            var idx = new AtomicLong(0l);
            ss.forEachOrdered(s -> System.out.format("%d: %s\n",idx.incrementAndGet(), s));
        }
    }
}

/**
 * Байтовая запись данных в файл.
 * Если мы хотим записать набор байтов, например число int - это набор из 4 байтов, то нужно 
 * использовать OutputStream
 *     OutputStream - это абстрактный класс, который является базовым для всех классов, реализующих
 *     байтовые потоки вывода
 * 
 * ByteArrayOutputStream -> OutputStream
 * ObjectOutputStream    -> OutputStream
 * FileOutputStream      -> OutputStream
 * 
 * BufferedOutputStream -> FilterOutputStream -> OutputStream
 * DataOutputStream     -> FilterOutputStream -> OutputStream
 * PrintStream          -> FilterOutputStream -> OutputStream
 * 
 * BufferedOutputStream - это класс, реализующий запись данных в байтовый массив, который автоматически 
 * будет увеличиваться в размере по мере добавления данных. [Массивы в Java имеют фиксированный размер, 
 * потому увеличение буфера требует дополнительного времени на перезапись все данных в новое место]
 * 
 * ObjectOutputStream - это специальный Wrapper (класс-обёртка), который производит действия по сериализации
 * данных объекта в поток, например в файл. Он должен получить в конструкторе реальный байтовый поток, куда 
 * будут отправлены данные. Обратим внимание, что с помощью метода writeObject можно записывать только такие
 * классы в поток, которые отмечены интерфейсом Serializable.
 * 
 * FileOutputStream - это поток для записи данных в файл.
 * 
 * Следующие классы являются обёртками над потоками для удобства записи данных в байтовый поток.
 * BufferedOutputStream - предоставляет буферизованный вывод.
 * DataOutputStream - записывает примитивные типы Java в байтовый поток.
 * PrintStream - позволяет записывать в поток данные удобным способом с форматированием. В частности 
 * этому классу наследует System.out. 
 * 
 */

final class ObjectToFileExample {
    public static void test(String filename) throws IOException {
        /**
         * Сам ObjectOutputStream не может записывать данные куда-либо,
         * ему необходима готовая реализация потока с настоящим буфером.
         */
        try (var oos = new ObjectOutputStream(new FileOutputStream(filename))) {
            String s = "Hello"; // строка сериализуема
            oos.writeObject(s);
            System.out.println("The object has been successfully written to a file!");
        }
        /**
         * Если мы отроем результат в шестнадцатиричном редакторе, то увидим представление
         * байтов:
         * AC ED 00 05 74 00 05 48 65 6C 6C 6F
         * 
         * Здесь байты AC ED 00 05 74 00 05 - это служебная информация Java
         * Байты 48 65 6C 6C 6F - это строка "Hello", записанная кодировкой UTF-8, которая для 
         * в данном случае совпадает с кодировкой ASCII.
         */
    }
}

/**
 * Прочитать данные из байтового потока можно с помощью InputStream.
 * Для InputStream есть похожая иерархия:
 *
 * ByteArrayInputStream -> InputStream
 * ObjectInputStream    -> InputStream
 * FileInputStream      -> InputStream
 * 
 * BufferedInputStream -> FilterInputStream -> InputStream
 * DataInputStream     -> FilterInputStream -> InputStream
 * 
 * Смысл классов такой же, как и для OutputStream, но теперь они применяются для ввода данных
 * в программу из потока.
 */

final class ObjectFromFileExample {
    public static void test(String filename) throws IOException {
        try (var ois = new ObjectInputStream(new FileInputStream(filename))) {
            // readObject возвращает ссылку на Object, мы должны сами привести её к ожидаемому типу
            // могут возникнуть две ошибки:
            //     класс невозможно загрузить в JVM, например не найден его байткод;
            //     прочитанный из потока тип не соответствует ожидаемому типу.
            var s = (String) ois.readObject();
            System.out.format("The object has been read: %s\n", s);
        } catch (ClassNotFoundException e) {
            System.err.println("Can't load the class for the serialized object!");
        } catch (ClassCastException e) {
            System.err.println("The expected class of an object does not equals class actually found in the stream");
        }
    }
} 

/**
 * В Java IO есть специальный тип файла, который позволяет перемещаться по нему в произвольное место.
 * Он поддерживается классом RandomAccessFile.
 * Строго говоря это зависит от возможностей операционной системы.
 * Продемонстрируем возможности такого класса.
 */
final class WriteToRandomAccessFileExample {
    private static record Author (String name, long taxId) { }

    private static void pushAuthor(Author a, RandomAccessFile os) throws IOException {
        byte[] nameBytes = a.name().getBytes("UTF-16");
        if (nameBytes.length > 64)
            throw new IllegalArgumentException("The Author name is too long, can't write to the table");
        byte[] actualBytes = new byte[64];
        System.arraycopy(nameBytes, 0, actualBytes, 0, nameBytes.length);
        os.write(actualBytes);
        os.writeLong(a.taxId());
    }

    private static Author readFromRAF(RandomAccessFile file, long authorNum) throws IOException { 
        file.seek((authorNum - 1) * 72); // skip
        byte[] authorNameBytes = new byte[64];
        file.read(authorNameBytes);
        String name = new String(authorNameBytes, "UTF-16");
        long taxId = file.readLong();
        return new Author(name, taxId);
    }

    public static void test(String filename) throws IOException {
        try (var raf = new RandomAccessFile(filename, "rw")) {
            var a1 = new Author("Ann", 1);
            var a2 = new Author("Bill", 2);
            var a3 = new Author("Sam", 13);
            pushAuthor(a1, raf);
            pushAuthor(a2, raf);
            pushAuthor(a3, raf);
        }

        try (var raf = new RandomAccessFile(filename, "r")) {
            Author a3 = readFromRAF(raf, 3);
            System.out.println(a3.toString());
            Author a1 = readFromRAF(raf, 1);
            System.out.println(a1.toString());
        }
    }
}

public class IOExample {
    public static void main(String[] args) throws IOException {
        if (false) FileWriterExample.test(args[0], args[1]); 
        if (false) StringWriterExample.test("Hello", "StringWriter");
        if (false) PrintWriterExample.test(args[0], new long[]{1, 2, 3, 4, 5, 6, 7});
        if (false) BufferedReaderExample.test(args[0]);
        if (false) BufferedReaderStreamExample.test(args[0]);
        if (false) ObjectToFileExample.test(args[0]);
        if (false) ObjectFromFileExample.test(args[0]);
        if (false) WriteToRandomAccessFileExample.test(args[0]);
    } 
}
